Jerry's Log

Keys vs Scan

contents

keys와 scan은 프로덕션 Redis 인스턴스를 관리할 때 이해해야 할 가장 중요한 차이점 중 하나입니다.

짧은 답변: KEYS는 위험한 블로킹(Blocking) 작업으로, 프로덕션 서버를 멈추게(Freeze) 할 수 있습니다. SCAN은 프로덕션 환경을 위해 설계된 안전한 논블로킹(Non-blocking) 대안입니다.

다음은 이들이 내부적으로 어떻게 작동하는지에 대한 상세한 비교입니다.


1. KEYS 명령어 (핵폭탄급 옵션) ☢️

KEYS pattern 명령어는 특정 패턴과 일치하는 모든 키를 한 번에 반환합니다.

작동 방식

Redis는 싱글 스레드(Single-threaded) 입니다. 즉, Redis는 한 번에 하나의 명령어만 처리합니다. 해당 명령어가 끝날 때까지 다른 작업은 아무것도 할 수 없습니다.

위험한 이유

사용해야 할 때


2. SCAN 명령어 (안전한 반복자) 🛡️

SCAN cursor [MATCH pattern] [COUNT count] 명령어를 사용하면 키를 점진적으로 순회(Iterate)할 수 있습니다.

작동 방식

한 번에 모든 것을 가져오는 대신, SCAN은 페이지네이션 시스템처럼 작동합니다.

  1. 요청: SCAN 0을 보냅니다.
  2. 응답: Redis는 아주 적은 양의 작업(내부 해시 테이블 버킷의 작은 섹션을 스캔)만 수행하고 다음을 반환합니다:
    • 새로운 커서 (New Cursor) (예: "145").
    • 해당 섹션에서 발견된 키 목록.
  3. 끼어들기 (Interleaving): 응답을 보낸 후, Redis는 자유로워져서 다른 요청(앱에서 오는 SET이나 GET 등)을 처리할 수 있습니다.
  4. 다음 요청: 방금 받은 커서를 사용하여 SCAN 145를 보냅니다.
  5. 반복: Redis가 0이라는 커서를 반환할 때까지 이를 반복합니다. (0은 순회가 끝났음을 의미).

안전한 이유

SCAN의 특이점 (중요!)

SCAN은 상태가 없습니다(Redis는 당신이 어디까지 읽었는지 기억하지 않으며, _커서_가 위치 정보를 인코딩하고 있습니다). 이로 인해 몇 가지 부작용이 있습니다.

  1. 중복 (Duplicates): 서로 다른 두 번의 SCAN 호출에서 동일한 키가 반환될 수 있습니다. 애플리케이션은 반드시 중복 제거를 처리해야 합니다(예: 키를 Set에 넣는 방식).
    • 이유: 스캔 중에 Redis가 해시 테이블 크기를 조정(Re-hashing)하면 버킷 구조가 바뀌기 때문입니다.
  2. 대략적인 개수: COUNT 옵션은 힌트일 뿐입니다. 10개를 요청했는데 15개를 받을 수도, 5개를 받을 수도, 혹은 0개를 받을 수도 있습니다(커서는 0이 아닌 상태로).

3. 나란히 비교

특징 KEYS SCAN
블로킹 여부? YES. 서버 전체를 멈춤. NO. 호출 사이에 다른 명령 실행 가능.
복잡도 한 번에 $O(N)$. 호출당 $O(1)$ (전체 $O(N)$).
반환 값 모든 키가 담긴 하나의 거대한 리스트. 커서 + 작은 키 리스트.
프로덕션 안전? 아니오. 예.
중복 발생? 없음. 유일성 보장. 있음. 중복 가능성 존재.
정확성 그 순간의 정확한 스냅샷. 스캔 도중에 생성/삭제된 키는 반환될 수도, 안 될 수도 있음.

4. 비유 🏟️

5만 명이 있는 거대한 경기장(Keys)에서 이름이 "철수"인 사람을 모두 찾는다고 상상해 봅시다.

5. 구현 코드 (Python 예제)

특정 키를 안전하게 삭제하기 위해 스크립트에서 스캔을 구현하는 방법입니다.

import redis

r = redis.Redis(host='localhost', port=6379)
cursor = '0'
pattern = 'user:*:session'

while cursor != 0:
    # SCAN은 [새_커서, [키_리스트]]를 반환합니다.
    cursor, keys = r.scan(cursor=cursor, match=pattern, count=100)
    
    if keys:
        # 이 배치의 키들을 처리합니다.
        r.delete(*keys)
        print(f"{len(keys)}개의 키 배치를 삭제했습니다.")

print("정리 완료.")

references